-- 2022-8-12

--[[ 16.0

    虽然我们把Lua语言称为解释性语言(interpreted language) 但Lua语言总是在运行代码前先预编译(precompile)源码为中间代码
    (这没什么大不了的 很多解释性语言也这样做)
    编译(compilation)阶段的存在听上去超出了解释性语言的范畴 但解释性语言的区分并不在于源码是否被编译 而在于是否有能力(且轻易的)执行动态生成的代码
    可以认为 正是由于诸如dofile这样函数的存在 才使得Lua语言能够被称为解释性语言
    在本章中 我们会详细学习Lua语言运行代码的过程 编译究竟是什么意思和做了什么 Lua语言是如何运行编译后代码的以及在编译过程中如何处理错误
]]

---[[ 16.1 编译

    -- 此前 我们已经介绍过函数dofile 它是运行代码段的主要方式之一
    -- 实际上 函数dofile是一个辅助函数 函数loadfile才完成了真正的核心工作
    -- 与函数dofile类似函数loadfile也是从文件中加载Lua代码段 但它不会运行代码 而只是编译代码 然后将编译后的代码段做为一个函数返回
    -- 此外与函数dofile不同 函数loadfile只返回错误码而不抛出异常 可以认为 函数dofile就是
    do
        function dofile (filename)
            local f = assert(loadfile(filename))
            return f()
        end
    end
    -- 请注意 如果函数loadfile执行失败 那么函数assert会引发一个错误

    -- 对于简单的需求而言 由于函数dofile在一次调用中就做完了所有工作 所以该函数非常易用 不过 函数loadfile更加灵活
    -- 在发生错误的情况中 函数loadfile会返回nil及错误信息 以允许我们按自定义的方式来处理错误
    -- 此外 如果需要多次运行同一个文件 那么只需调用一次loadfile函数后再多次调用它的返回结果即可
    -- 由于只编译一次文件 因此这种方式的开销要比多次调用dofile小得多(编译在某种程度上相比其他操作开销更大)

    -- 函数load和函数loadfile类似 不同之处在于该函数从一个字符串或函数中读取代码段 而不是从文件中读取 例如 考虑如下代码
    local f = load("i = i + 1")
    -- 在这句代码执行后 变量f就会变成一个被调用时执行i = i + 1的函数
    if(f~=nil) then
         i = 0
        f(); print(i)
        f(); print(i)
    end

    -- 尽管函数load的功能很强大 但还是应该谨慎的使用 相对于其他可选的函数而言 该函数的开销更大并且可能会引起诡异的问题
    -- 请先确定当下已经找不到更简单的解决方式后再使用该函数
    -- 如果要编写一个用后即弃的dostring函数(例如加载并运行一段代码) 那么我们可以直接调用函数load的返回值
    load(s)()
    -- 不过 如果代码中有语法错误 函数load就会返回nil和形如"试图调用一个nil值(attempt to call a nil value)"的错误信息
    -- 为了更清楚的展示错误信息 最好使用函数assert:
    assert(load(s))()
    -- 通常 用函数load来加载字符串常量是没有意义的 列如 如下的两行代码基本等价
    local f = load("i = i + 1")
    local f = function () i = i + 1 end
    -- 但是由于第2行代码会与其外层的函数一起被编译 所以其执行速度要快得多 与之对比 第一段代码在调用函数load时会进行一次独立的编译
    -- 由于函数load在编译时不涉及词法定界 所以上述示例的两段代码可能并不完全等价
    -- 为了清晰地展示它们之间的区别 让我们稍微修改一下上面的例子
    i = 32
    local i = 0
    f = load("i = i + 1; print(i)")
    g = function () i = i + 1; print(i) end
    f()     --> 33
    g()     --> 1
    -- 函数g像我们所预期的那样操作局部变量i 但函数f操作的却是全局变量i 这是由于函数load总是在全局环境中编译代码段

    -- 函数load最典型的用法是执行外部代码(即那些来自程序本身之外的代码段)或动态生成的代码
    -- 例如 我们可能想运行用户定义的函数 由用户输入函数的代码后调用函数load对其求值
    -- 请注意 函数load期望的输入是一段程序 也就是一系列的语句
    -- 如果需要对表达式求值 那么可以在表达式前添加return 这样才能构造一条返回指定表达式值的语句 例如:
    print "enter your expression:"
    local line = io.read()
    local func = assert(load("return "..line))
    print("the value of your expression is " .. func())
    -- 由于函数load所返回的函数就是一个普通函数 因此可以反复对其进行调用
    print "enter function to be plotted (with variable 'x'):"
    local line = io.read()
    local f = assert(load("return "..line))
    for i = 1, 20 do
        x = i  -- 全局的'x'(当前代码段内可见)
        print(string.rep("*",f()))
    end
    -- 我们也可以使用读取函数(reader function)作为函数load的第1个参数
    -- 读取函数可以分几次返回一段程序 函数load会不断的调用读取函数直到读取函数返回nil(表示程序段结束)
    -- 作为示例 以下的调用与函数loadfile等价
    f = load(io.lines(filenamem,"*L"))
    -- 正如我们在第七章中所看到的 调用io.lines(filename,"*L")返回一个函数 这个函数每次被调用时就从指定文件返回一行
    -- 因此 函数load会一行一行的从文件中读取一段程序 以下的版本与之相似但效率稍高
    f = load(io.lines(filename,1024))
    -- 这里 函数io.lines返回的迭代器会以1024字节为块读取源文件
    -- Lua语言将所有独立的代码段当作匿名可变长参数函数的函数体 例如load("a=1")的返回值与以下表达式等价
    -- function (...) a = 1 end
    -- 像其他任何函数一样 代码段中可以声明局部变量
    f = load("local a = 10; print(a + 20)")
    f()
    -- 使用这个特性 可以在不使用全局变量x的情况下重写之前运行用户定义函数的示例
    print "enter function to be plotted (with variable 'x'):"
    local line = io.read()
    local f = assert(load("local x = ...; return " .. line))
    for i = 1, 20 do
        print(string.rep("*",f(i)))
    end
    -- 在上述代码中 在代码段开头增加了"local x = ..."来将x声明为局部变量
    -- 之后使用参数i调用函数f 参数i就是可变长参数的表达式的值(...)
    -- 函数load和函数loadfile从来不引发错误 当有错误发生时 它们会返回nil及错误信息:
    print(load("i i"))
    --> nil [string "i i"]:1:1 '=' expected near 'i'
    -- 此外 这些函数没有任何副作用 它们既不改变或创建变量 也不向文件写入等
    -- 这些函数只是将程序段编译为一种中间形式 然后将结果作为匿名函数返回
    -- 一种常见的误解是认为加载一段程序也就是定义了函数 但实际上在Lua语言中函数定义是在运行时而不是在编译时发生的一种赋值操作
    -- 例如 假设有一个文件foo.lua
        -- 文件foo.lua
    function foo(x)
        print(x)
    end
    -- 当执行
    f = loadfile("foo.lua")
    -- 时 编译foo的命令并没有定义foo 只有运行代码才会定义它:
    f = loadfile("foo.lua")
    print(foo) --> nil
    f()
    foo("ok") --> ok
    -- 这种行为可能看上去有些奇怪 但如果不使用语法糖对其进行重写则看上去会清晰很多
        -- 文件foo.lua
    foo = function (x)
        print(x)
    end
    -- 如果线上产品级别的程序需要执行外部代码 那么应该处理加载程序段时报告的所有错误
    -- 此外 为了避免不愉快的副作用发生 可能还应该在一个受保护的环境中执行这些代码
    -- 我们会在22章讨论这些细节
--]]